// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/bluetooth/bluetooth_allowed_devices_map.h"

#include <string>
#include <vector>

#include "base/logging.h"
#include "base/optional.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "content/browser/bluetooth/bluetooth_blocklist.h"
#include "content/common/bluetooth/web_bluetooth_device_id.h"

using device::BluetoothUUID;

namespace content {

BluetoothAllowedDevicesMap::BluetoothAllowedDevicesMap() { }
BluetoothAllowedDevicesMap::~BluetoothAllowedDevicesMap() { }

const WebBluetoothDeviceId& BluetoothAllowedDevicesMap::AddDevice(
    const url::Origin& origin,
    const std::string& device_address,
    const blink::mojom::WebBluetoothRequestDeviceOptionsPtr& options)
{
    DVLOG(1) << "Adding a device to Map of Allowed Devices.";

    // "Unique" Origins generate the same key in maps, therefore are not
    // supported.
    CHECK(!origin.unique());

    auto device_address_to_id_map = origin_to_device_address_to_id_map_[origin];
    auto id_iter = device_address_to_id_map.find(device_address);
    if (id_iter != device_address_to_id_map.end()) {
        DVLOG(1) << "Device already in map of allowed devices.";
        const auto& device_id = id_iter->second;

        AddUnionOfServicesTo(
            options, &origin_to_device_id_to_services_map_[origin][device_id]);

        return origin_to_device_address_to_id_map_[origin][device_address];
    }
    const WebBluetoothDeviceId device_id = GenerateUniqueDeviceId();
    DVLOG(1) << "Id generated for device: " << device_id;

    origin_to_device_address_to_id_map_[origin][device_address] = device_id;
    origin_to_device_id_to_address_map_[origin][device_id] = device_address;
    AddUnionOfServicesTo(
        options, &origin_to_device_id_to_services_map_[origin][device_id]);

    CHECK(device_id_set_.insert(device_id).second);

    return origin_to_device_address_to_id_map_[origin][device_address];
}

void BluetoothAllowedDevicesMap::RemoveDevice(
    const url::Origin& origin,
    const std::string& device_address)
{
    const WebBluetoothDeviceId* device_id_ptr = GetDeviceId(origin, device_address);
    DCHECK(device_id_ptr != nullptr);

    // We make a copy because we are going to remove the original value from its
    // map.
    WebBluetoothDeviceId device_id = *device_id_ptr;

    // 1. Remove from all three maps.
    CHECK(origin_to_device_address_to_id_map_[origin].erase(device_address));
    CHECK(origin_to_device_id_to_address_map_[origin].erase(device_id));
    CHECK(origin_to_device_id_to_services_map_[origin].erase(device_id));

    // 2. Remove empty map for origin.
    if (origin_to_device_address_to_id_map_[origin].empty()) {
        CHECK(origin_to_device_address_to_id_map_.erase(origin));
        CHECK(origin_to_device_id_to_address_map_.erase(origin));
        CHECK(origin_to_device_id_to_services_map_.erase(origin));
    }

    // 3. Remove from set of ids.
    CHECK(device_id_set_.erase(device_id));
}

const WebBluetoothDeviceId* BluetoothAllowedDevicesMap::GetDeviceId(
    const url::Origin& origin,
    const std::string& device_address)
{
    auto address_map_iter = origin_to_device_address_to_id_map_.find(origin);
    if (address_map_iter == origin_to_device_address_to_id_map_.end()) {
        return nullptr;
    }

    const auto& device_address_to_id_map = address_map_iter->second;

    auto id_iter = device_address_to_id_map.find(device_address);
    if (id_iter == device_address_to_id_map.end()) {
        return nullptr;
    }
    return &(id_iter->second);
}

const std::string& BluetoothAllowedDevicesMap::GetDeviceAddress(
    const url::Origin& origin,
    const WebBluetoothDeviceId& device_id)
{
    auto id_map_iter = origin_to_device_id_to_address_map_.find(origin);
    if (id_map_iter == origin_to_device_id_to_address_map_.end()) {
        return base::EmptyString();
    }

    const auto& device_id_to_address_map = id_map_iter->second;

    auto id_iter = device_id_to_address_map.find(device_id);

    return id_iter == device_id_to_address_map.end() ? base::EmptyString()
                                                     : id_iter->second;
}

bool BluetoothAllowedDevicesMap::IsOriginAllowedToAccessAtLeastOneService(
    const url::Origin& origin,
    const WebBluetoothDeviceId& device_id) const
{
    auto id_map_iter = origin_to_device_id_to_services_map_.find(origin);
    if (id_map_iter == origin_to_device_id_to_services_map_.end()) {
        return false;
    }

    const auto& device_id_to_services_map = id_map_iter->second;

    auto id_iter = device_id_to_services_map.find(device_id);

    return id_iter == device_id_to_services_map.end() ? false
                                                      : !id_iter->second.empty();
}

bool BluetoothAllowedDevicesMap::IsOriginAllowedToAccessService(
    const url::Origin& origin,
    const WebBluetoothDeviceId& device_id,
    const BluetoothUUID& service_uuid) const
{
    if (BluetoothBlocklist::Get().IsExcluded(service_uuid)) {
        return false;
    }

    auto id_map_iter = origin_to_device_id_to_services_map_.find(origin);
    if (id_map_iter == origin_to_device_id_to_services_map_.end()) {
        return false;
    }

    const auto& device_id_to_services_map = id_map_iter->second;

    auto id_iter = device_id_to_services_map.find(device_id);

    return id_iter == device_id_to_services_map.end()
        ? false
        : base::ContainsKey(id_iter->second, service_uuid);
}

WebBluetoothDeviceId BluetoothAllowedDevicesMap::GenerateUniqueDeviceId()
{
    WebBluetoothDeviceId device_id = WebBluetoothDeviceId::Create();
    while (base::ContainsKey(device_id_set_, device_id)) {
        LOG(WARNING) << "Generated repeated id.";
        device_id = WebBluetoothDeviceId::Create();
    }
    return device_id;
}

void BluetoothAllowedDevicesMap::AddUnionOfServicesTo(
    const blink::mojom::WebBluetoothRequestDeviceOptionsPtr& options,
    std::unordered_set<BluetoothUUID, device::BluetoothUUIDHash>*
        unionOfServices)
{
    if (options->filters) {
        for (const auto& filter : options->filters.value()) {
            if (!filter->services) {
                continue;
            }

            for (const BluetoothUUID& uuid : filter->services.value()) {
                unionOfServices->insert(uuid);
            }
        }
    }

    for (const BluetoothUUID& uuid : options->optional_services) {
        unionOfServices->insert(uuid);
    }
}

} // namespace content
